﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MemoryUtil;
using System.Collections.ObjectModel;
using PlayerScannerCore.Properties;

namespace PlayerScannerCore.Model
{
    /// <summary>
    /// Represents the heart of the application where memory reads are actually performed.
    /// </summary>
    public class Model
    {
        private const float TECH_SCALE = 0.013f;
        private const float TECH_BASE = 0.1f;
        private const int PSO_SECONDS_PER_SECOND = 30;
        private const int CHAR_STAT_BONUS_RECORD_SIZE = 0x30;
        private const int MAX_STATS_RECORD_SIZE = 0x24;
        private const int MAX_PLAYERS = 15;

        //Base Addresses. These are the ones that need fixing when a patch comes out
        private readonly BaseAddress BASE_PLAYER_ARRAY_ADDR = new BaseAddress(0x00a94254);
        private readonly BaseAddress BASE_LEADER_ADDR = new BaseAddress(0x00a9c4dc);
        private readonly BaseAddress BASE_NUM_PLAYERS_ADDR = new BaseAddress(0x00aae168);
        //private readonly BaseAddress BASE_CHAR_STAT_BONUS_TABLE_ADDR = new BaseAddress(0x00966a40);
        //private readonly BaseAddress BASE_MAX_STATS_ARRAY_ADDR = new BaseAddress(0x00966540);
        private readonly BaseAddress BASE_EXP_TABLE_ADDR = new BaseAddress(0x00a94878);

        private const int PLAYER_CONDITONS = 0x30;
        private const int PLAYER_APP_TECH_1 = 0x274;
        private const int PLAYER_APP_TECH_1_STRENGTH = 0x278;
        private const int PLAYER_APP_TECH_1_TIME = 0x27C;
        private const int PLAYER_APP_TECH_2 = 0x280;
        private const int PLAYER_APP_TECH_2_STRENGTH = 0x284;
        private const int PLAYER_APP_TECH_2_TIME = 0x288;
        private const int PLAYER_SUB_RECORD = 0X2b4;
        private const int PLAYER_MAX_HP = 0x2BC;
        private const int PLAYER_MAX_TP = 0x2BE;

        //Stat offsets
        private const int PLAYER_LCK = 0x2D6;
        private const int PLAYER_ATA = 0x2D4;
        private const int PLAYER_DEF = 0x2D2;
        private const int PLAYER_EVP = 0x2D0;

        //Resistance offsets
        private const int PLAYER_EFR = 0x2f6;
        private const int PLAYER_EIC = 0x2fa;
        private const int PLAYER_ETH = 0x2f8;
        private const int PLAYER_EDK = 0x2fc;
        private const int PLAYER_ELT = 0x2fe;

        //Material offsets
        private const int PLAYER_MAT_HP = 0x2C0;
        private const int PLAYER_MAT_TP = 0x2C2;
        private const int PLAYER_MAT_POW = 0x384;
        private const int PLAYER_MAT_DEF = 0x387;
        private const int PLAYER_MAT_MST = 0x385;
        private const int PLAYER_MAT_EVP = 0x386;
        private const int PLAYER_MAT_LCK = 0x388;
        
        //Tech offsets
        private const int PLAYER_TECH_FOIE = 0x4A8;
        private const int PLAYER_TECH_GIFOIE = 0x4A9;
        private const int PLAYER_TECH_RAFOIE = 0x4AA;
        private const int PLAYER_TECH_BARTA = 0x4AB;
        private const int PLAYER_TECH_GIBARTA = 0x4AC;
        private const int PLAYER_TECH_RABARTA = 0x4AD;
        private const int PLAYER_TECH_ZONDE = 0x4AE;
        private const int PLAYER_TECH_GIZONDE = 0x4AF;
        private const int PLAYER_TECH_RAZONDE = 0x4B0;
        private const int PLAYER_TECH_GRANTS = 0x4B1;
        private const int PLAYER_TECH_DEBAND = 0x4B2;
        private const int PLAYER_TECH_JELLEN = 0x4B3;
        private const int PLAYER_TECH_ZALURE = 0x4B4;
        private const int PLAYER_TECH_SHIFTA = 0x4B5;
        private const int PLAYER_TECH_RYUKER = 0x4B6;
        private const int PLAYER_TECH_RESTA = 0x4B7;
        private const int PLAYER_TECH_ANTI = 0x4B8;
        private const int PLAYER_TECH_REVERSER = 0x4B9;
        private const int PLAYER_TECH_MEGID = 0x4BA;

        //Photon Blast
        //The actual PB charge is only non-0 for the current character!
        private const int PLAYER_PB_CHARGE = 0x520;
        private const int PLAYER_PB_IS_100 = 0x524;

        //Trap offsets
        private const int PLAYER_TRAP_DAMAGE = 0x89c;
        private const int PLAYER_TRAP_FREEZE = 0x89d;
        private const int PLAYER_TRAP_SLOW = 0x89e;
        private const int PLAYER_TRAP_CONFUSE = 0x89f;

        //Misc offsets
        private const int PLAYER_SECTION_ID = 0x960;
        private const int PLAYER_CHARACTER_ID = 0x961;
        private const int PLAYER_NAME = 0x984;
        private const int PLAYER_GUILDCARD = 0xeb4;

        //Character bonus stat table offsets
        private const int CHAR_BONUS_ATP = 0x2;
        private const int CHAR_BONUS_ATA = 0x4;

        //Max stat offsets
        private const int MAX_STAT_ATP = 0x0;
        private const int MAX_STAT_MST = 0x2;
        private const int MAX_STAT_EVP = 0x4;
        private const int MAX_STAT_DEF = 0x8;
        private const int MAX_STAT_ATA = 0xa;
        private const int MAX_STAT_LCK = 0xc;

        //Sub record stat offsets
        private const int SUB_RECORD_ATP = 0x0;
        private const int SUB_RECORD_MST = 0x2;
        private const int SUB_RECORD_EVP = 0x4;
        private const int SUB_RECORD_ATA = 0xA;
        private const int SUB_RECORD_DEF = 0x8;
        private const int SUB_RECORD_LCK = 0xc;
        private const int SUB_RECORD_LEVEL = 0x18;
        private const int SUB_RECORD_MESTA = 0x20;
        private const int SUB_RECORD_TOTAL_EXP = 0x1c;

        //Special offset for power stat boost
        private const int SUB_RECORD_ATP_BOOST = 0x26;

        private ProcessMemory _processMemory;
        private ItemReader _itemReader;
        private LabelReader _labelReader;

        internal PlayerFactory PlayerFactory { get { return Factory.Instance.PlayerFactory; } }

        internal Model()
        {
            _processMemory = new ProcessMemory(Settings.Default.TargetProcessName, AccessMode.Read);
            _labelReader = new LabelReader(_processMemory);
            _itemReader = new ItemReader(_processMemory, _labelReader);

            //This will be changed when a patch comes out
            ShiftBaseAddressesBy(0);
        }

        public bool IsAttached
        {
            get { return _processMemory.IsOpen; }
        }

        internal void AttachToPso()
        {
            _processMemory.TryOpen();
        }

        public void ShiftBaseAddressesBy(int offset)
        {
            BaseAddress.ShiftAllBaseAddressesBy(offset);
        }

        internal RoomInfo GetRoomInfo()
        {
            var playerIndex = _processMemory.ReadInt32(BASE_LEADER_ADDR);
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));

            var ret = new RoomInfo();

            if (playerRecordAddress != 0)
            {
                var sectionIdIndex = _processMemory.ReadByte(playerRecordAddress + PLAYER_SECTION_ID);

                ret.LeaderName = _processMemory.ReadNullTerminatedString(playerRecordAddress + PLAYER_NAME, 50);
                ret.LeaderSectionId = _labelReader.GetText(TextType.SectionId, sectionIdIndex);
            }
            else
            {
                ret.LeaderName = "Unknown";
                ret.LeaderSectionId = "Unknown";
            }

            return ret;
        }

        internal PlayerTechInfo GetPlayerTechInfo(int playerIndex)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));

            return new PlayerTechInfo()
            {
                Foie = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_FOIE) + 1,
                Gifoie = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_GIFOIE) + 1,
                Rafoie = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_RAFOIE) + 1,
                Barta = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_BARTA) + 1,
                Gibarta = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_GIBARTA) + 1,
                Rabarta = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_RABARTA) + 1,
                Zonde = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_ZONDE) + 1,
                Gizonde = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_GIZONDE) + 1,
                Razonde = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_RAZONDE) + 1,
                Resta = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_RESTA) + 1,
                Anti = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_ANTI) + 1,
                Reverser = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_REVERSER) + 1,
                Ryuker = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_RYUKER) + 1,
                Shifta = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_SHIFTA) + 1,
                Deband = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_DEBAND) + 1,
                Jellen = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_JELLEN) + 1,
                Zalure = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_ZALURE) + 1,
                Grants = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_GRANTS) + 1,
                Megid = _processMemory.ReadSByte(playerRecordAddress + PLAYER_TECH_MEGID) + 1
            };
        }

        internal PlayerTrapInfo GetPlayerTrapInfo(int playerIndex)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));

            return new PlayerTrapInfo()
            {
                Damage = _processMemory.ReadByte(playerRecordAddress + PLAYER_TRAP_DAMAGE),
                Freeze = _processMemory.ReadByte(playerRecordAddress + PLAYER_TRAP_FREEZE),
                Confuse = _processMemory.ReadByte(playerRecordAddress + PLAYER_TRAP_CONFUSE),
                Slow = _processMemory.ReadByte(playerRecordAddress + PLAYER_TRAP_SLOW)
            };
        }

        internal PlayerMaterialInfo GetPlayerMaterialInfo(int playerIndex)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));

            return new PlayerMaterialInfo()
            {
                Power = _processMemory.ReadByte(playerRecordAddress + PLAYER_MAT_POW),
                Defense = _processMemory.ReadByte(playerRecordAddress + PLAYER_MAT_DEF),
                Mind = _processMemory.ReadByte(playerRecordAddress + PLAYER_MAT_MST),
                Evasion = _processMemory.ReadByte(playerRecordAddress + PLAYER_MAT_EVP),
                Luck = _processMemory.ReadByte(playerRecordAddress + PLAYER_MAT_LCK),
                Hp = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_MAT_HP) / 2,
                Tp = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_MAT_TP) / 2
            };
        }

        internal PlayerStatsInfo GetPlayerStatsInfo(int playerIndex)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));
            var playerCharacter = _processMemory.ReadByte(playerRecordAddress + PLAYER_CHARACTER_ID);
            //var statBonusAddress = BASE_CHAR_STAT_BONUS_TABLE_ADDR + (playerCharacter * CHAR_STAT_BONUS_RECORD_SIZE);
            var subRecordAddress = _processMemory.ReadUInt32(playerRecordAddress + PLAYER_SUB_RECORD);
            //var maxStatsAddress = BASE_MAX_STATS_ARRAY_ADDR + (MAX_STATS_RECORD_SIZE * playerCharacter);

            //Get the stats the player receives as a bonus from his character type
            //var atpBonus = _processMemory.ReadChar(statBonusAddress + CHAR_BONUS_ATP);
            //var ataBonus = _processMemory.ReadChar(statBonusAddress + CHAR_BONUS_ATA);

            var atpBaseRaw = _processMemory.ReadUInt16(subRecordAddress + SUB_RECORD_ATP);
            var mstBaseRaw = _processMemory.ReadUInt16(subRecordAddress + SUB_RECORD_MST);

            return new PlayerStatsInfo()
            {
                Power = _processMemory.ReadUInt16(subRecordAddress + SUB_RECORD_ATP_BOOST) + atpBaseRaw,
                BasePower = 0,//_processMemory.ReadChar(subRecordAddress + SUB_RECORD_ATP) + atpBonus,
                MaxPower = 0,//_processMemory.ReadChar(maxStatsAddress + MAX_STAT_ATP) + atpBonus,
                Defense = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_DEF),
                BaseDefense = 0,//_processMemory.ReadChar(subRecordAddress + SUB_RECORD_DEF),
                MaxDefense = 0,//_processMemory.ReadChar(maxStatsAddress + MAX_STAT_DEF),
                Accuracy = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_ATA),
                BaseAccuracy = 0,//(_processMemory.ReadChar(subRecordAddress + SUB_RECORD_ATA) + ataBonus) / 10f,
                MaxAccuracy = 0,//(_processMemory.ReadChar(maxStatsAddress + MAX_STAT_ATA) + ataBonus) / 10f,
                Mind = mstBaseRaw,
                BaseMind = 0,//mstBaseRaw,
                MaxMind = 0,//_processMemory.ReadChar(maxStatsAddress + MAX_STAT_MST),
                Evasion = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_EVP),
                BaseEvasion = 0,//_processMemory.ReadChar(subRecordAddress + SUB_RECORD_EVP),
                MaxEvasion = 0,//_processMemory.ReadChar(maxStatsAddress + MAX_STAT_EVP),
                Luck = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_LCK),
                BaseLuck = 0,//_processMemory.ReadChar(subRecordAddress + SUB_RECORD_LCK),
                MaxLuck = 0,//_processMemory.ReadChar(maxStatsAddress + MAX_STAT_LCK)
                Hp = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_MAX_HP),
                Tp = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_MAX_TP)
            };
        }

        internal PlayerResistanceInfo GetPlayerResistanceInfo(int playerIndex)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));

            return new PlayerResistanceInfo()
            {
                Efr = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_EFR),
                Eic = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_EIC),
                Eth = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_ETH),
                Edk = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_EDK),
                Elt = _processMemory.ReadUInt16(playerRecordAddress + PLAYER_ELT)
            };
        }

        internal PlayerMiscInfo GetPlayerMiscInfo(int playerIndex)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));
            var sectionIdIndex = _processMemory.ReadByte(playerRecordAddress + PLAYER_SECTION_ID);
            var playerCharacter = _processMemory.ReadByte(playerRecordAddress + PLAYER_CHARACTER_ID);
            var subRecordAddress = _processMemory.ReadUInt32(playerRecordAddress + PLAYER_SUB_RECORD);
            var expTableAddress = _processMemory.ReadUInt32(BASE_EXP_TABLE_ADDR);
            expTableAddress = _processMemory.ReadUInt32(expTableAddress);
            expTableAddress = _processMemory.ReadUInt32(expTableAddress + (uint)(4 * playerCharacter));
            var totalExp = _processMemory.ReadInt32(subRecordAddress + SUB_RECORD_TOTAL_EXP);
            var lvl = _processMemory.ReadInt32(subRecordAddress + SUB_RECORD_LEVEL) + 1;
            var nextLevelTotalExp = _processMemory.ReadInt32(expTableAddress + (uint)((12 * lvl) + 8));

            return new PlayerMiscInfo()
            {
                Character = _labelReader.GetText(TextType.CharacterId, playerCharacter),
                GuildCard = _processMemory.ReadInt32(playerRecordAddress + PLAYER_GUILDCARD),
                TotalExp = totalExp,
                ExpTillNextLevel = nextLevelTotalExp == 0 ? 0 : nextLevelTotalExp - totalExp,
                SectionId = _labelReader.GetText(TextType.SectionId, sectionIdIndex)
            };
        }

        internal int GetPlayerMeseta(int playerIndex)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * playerIndex));
            var subRecordAddress = _processMemory.ReadUInt32(playerRecordAddress + PLAYER_SUB_RECORD);

            return _processMemory.ReadInt32(subRecordAddress + SUB_RECORD_MESTA);
        }

        internal List<IItem> GetPlayerInventory(int playerIndex)
        {
            return _itemReader.GetPlayerInventory(playerIndex);
        }

        internal List<IItem> GetFloorItems()
        {
            return _itemReader.GetFloorItems();
        }

        /// <summary>
        /// Gets a list of players, populating only the PlayerName and Index properties for 
        /// identification purposes.
        /// </summary>
        internal List<IPlayerIdentifier> GetPlayerList()
        {
            var ret = new List<IPlayerIdentifier>();

            for (int i = 0; i < 15; i++)
            {
                var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * i));
                if (playerRecordAddress != 0)
                {
                    var player = PlayerFactory.CreatePlayer();
                    player.Index = i;
                    player.PlayerName = _processMemory.ReadNullTerminatedString(playerRecordAddress + PLAYER_NAME, 50);

                    ret.Add(player);
                }
            }

            return ret;
        }

        internal BasicPlayerInfo GetBasicPlayerInfo(int index)
        {
            var playerRecordAddress = _processMemory.ReadUInt32(BASE_PLAYER_ARRAY_ADDR + (uint)(4 * index));

            var appliedTech1 = _processMemory.ReadByte(playerRecordAddress + PLAYER_APP_TECH_1);
            var appliedTech1Strength = Convert.ToInt32(Math.Round((Math.Abs(_processMemory.ReadSingle(playerRecordAddress + PLAYER_APP_TECH_1_STRENGTH)) - TECH_BASE) / TECH_SCALE)) + 1;
            var appliedTech1Time = TimeSpan.FromSeconds(_processMemory.ReadInt32(playerRecordAddress + PLAYER_APP_TECH_1_TIME) / PSO_SECONDS_PER_SECOND);
            var appliedTech2 = _processMemory.ReadByte(playerRecordAddress + PLAYER_APP_TECH_2);
            var appliedTech2Strength = Convert.ToInt32(Math.Round((Math.Abs(_processMemory.ReadSingle(playerRecordAddress + PLAYER_APP_TECH_2_STRENGTH)) - TECH_BASE) / TECH_SCALE)) + 1;
            var appliedTech2Time = TimeSpan.FromSeconds(_processMemory.ReadInt32(playerRecordAddress + PLAYER_APP_TECH_2_TIME) / PSO_SECONDS_PER_SECOND);

            var appliedShifta = 0;
            var appliedDeband = 0;
            var appliedShiftaTime = new TimeSpan();
            var appliedDebandTime = new TimeSpan();

            //Is the first applied tech shifta or deband?
            switch (appliedTech1)
            {
            case 0x09:  //Shifta
                appliedShifta = appliedTech1Strength;
                appliedShiftaTime = appliedTech1Time;
                break;
            case 0x0A:  //Deband
                appliedDeband = appliedTech1Strength;
                appliedDebandTime = appliedTech1Time;
                break;
            }

            //Is the second applied tech shifta or deband?
            switch (appliedTech2)
            {
            case 0x09:  //Shifta
                appliedShifta = appliedTech2Strength;
                appliedShiftaTime = appliedTech2Time;
                break;
            case 0x0A:  //Deband
                appliedDeband = appliedTech2Strength;
                appliedDebandTime = appliedTech2Time;
                break;
            }
            //If they're the same (shouldn't happen)... who cares.

            var ret = new BasicPlayerInfo()
            {
                AppliedShifta = appliedShifta,
                AppliedDeband = appliedDeband,
                AppliedShiftaTime = appliedShiftaTime,
                AppliedDebandTime = appliedDebandTime,
                IsPhotonBlastCharged = _processMemory.ReadByte(playerRecordAddress + PLAYER_PB_IS_100) == 0x02
            };

            return ret;
        }
    }
}
